/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package axil.engine;

import axil.api.AxilType;
import axil.api.extend.SourceAdapter;
import axil.api.extend.SourceAdapter.Kind;
import axil.definition.*;
import axil.etc.Compressed;
import axil.etc.IdentityMap;
import axil.framework.type.NativeEquivalent;
import axil.framework.validation.ValidationContext;

import java.io.Reader;
import java.util.*;

import static axil.framework.Functions.*;


/**
 * The repository maintains a list of all known modules, functions, operators
 * and constants. The actual elements available for any given script will depend
 * on the modules used in that configuration. Using this model, we can restrict
 * what is available, but also define things only once.
 */
public class Repository implements Compressed {
	private static final Repository instance = new Repository();

	private final IdentityMap<Module> modules;
	private final IdentityMap<FunctionDefinition> functions;
	private final IdentityMap<OperatorDefinition> operators;
	private final IdentityMap<ConstantDefinition> constants;
	private final IdentityMap<TypeDefinition> types;
	private final Map<Class, AxilType> classes;
	private final Map<String, UnitDefinition> units;
	private final Map<Integer,String> registry;
	private int counter;


	private Repository() {
		this.modules = new IdentityMap<Module>();
		this.functions = new IdentityMap<FunctionDefinition>();
		this.operators = new IdentityMap<OperatorDefinition>();
		this.constants = new IdentityMap<ConstantDefinition>();
		this.types = new IdentityMap<TypeDefinition>();
		this.classes = new HashMap<Class, AxilType>();
		this.units = new HashMap<String, UnitDefinition>();
		this.registry = new HashMap<Integer, String>();
		this.counter = 0;
	}


	public static Repository instance() {
		return instance;
	}


	private Module mod(String ident) {
		for (SourceAdapter src : AxilEngine.instance().sources()) {
			Reader in = src.module(ident);
			if (in != null) {
				Module m = xml(Module.class, src.reference(in), in);
				close(in);
				return m;
			}
		}
		throw abort("Cannot find the specified module.", nv("module", ident));
	}


	public synchronized Module module(String ident) {
		Module module = modules.get(ident);
		if (module == null) {
			module = mod(ident);
			modules.put(module);

			/*
			 * Now we load all of the modules that this module depends upon.
			 * We keep doing this recursively until all of the requirements have
			 * been met. We do this up front so that all of the functions and
			 * such will be loaded before we attempt anything here.
			 */
			for (String depends : module.dependencies()) {
				module(depends);
			}
			ValidationContext context = new ValidationContext();
			module.validate(context);
			if (! context.ok()) {
				throw abort("The module is not valid.\n" + context.toString(),
				            nv("module", module.identity()));
			}
		}
		return module;
	}


	private <T> T load(Class<T> type, String module, Kind kind, String ident) {
		for (SourceAdapter src : AxilEngine.instance().sources()) {
			Reader in = src.definition(module, kind, ident);
			if (in != null) {
				T def = xml(type, src.reference(in), in);
				close(in);
				return def;
			}
		}
		throw abort("Cannot find the specified definition.",
		            nv("module", module), nv("kind", kind), nv("definition", ident));
	}


	public Set<String> dependencies(String module) {
		return walk(module(module), new HashSet<String>());
	}


	private Set<String> walk(Module module, Set<String> modules) {
		modules.add(module.identity());
		for (String depend : module.dependencies()) {
			if (! modules.contains(depend)) {
				walk(module(depend), modules);
			}
		}
		return modules;
	}


	public OperatorDefinition operator(String op) {
		return operators.get(op);
	}


	public OperatorDefinition operator(String module, String op) {
		OperatorDefinition o = operators.get(op);
		if (o == null) {
			o = load(OperatorDefinition.class, module, Kind.OPERATOR, op);
			operators.put(o);
		}
		return o;
	}


	public FunctionDefinition function(String module, String func) {
		FunctionDefinition f = functions.get(func);
		if (f == null) {
			f = load(FunctionDefinition.class, module, Kind.FUNCTION, func);
			functions.put(f);
		}
		return f;
	}


	public FunctionDefinition function(String func) {
		return functions.get(func);
	}


	public List<FunctionDefinition> functions() {
		return functions.values();
	}


	public synchronized TypeDefinition type(String module, String type) {
		TypeDefinition t = types.get(type);
		if (t == null) {
			t = load(TypeDefinition.class, module, Kind.TYPE, type);
			AxilType at = t.type();
			if (at instanceof NativeEquivalent) {
				NativeEquivalent ne = (NativeEquivalent)at;
				for (Class c : ne.natives()) {
					classes.put(c, at);
				}
			}
			types.put(t);
		}
		return t;
	}


	public AxilType type(String ident) {
		TypeDefinition type = types.get(ident);
		if (type == null) {
			throw abort("There is no registered type with that name.",
			            nv("type", ident));
		}
		return type.type();
	}


	public AxilType type(Object object) {
		return type(require(object).getClass());
	}


	public AxilType type(Class type) {
		AxilType t = classes.get(type);
		if (t == null) {
			throw abort("There is no Axil type corresponding to that class.",
			            nv("type", type));
		}
		return t;
	}


	public AxilType find(Class type) {
		return classes.get(type);
	}


	public AxilType find(String ident) {
		TypeDefinition def = types.get(ident);
		return def == null ? null : def.type();
	}


	/**
	 * Get the unit that matches the given identity. If There is no such unit,
	 * then a null is returned.
	 */
	public UnitDefinition unit(String ident) {
		return units.get(ident);
	}


	/**
	 * Register this unit definition. This is done only once when the unit is
	 * first loaded. We register the canonical identity as well as all aliases.
	 * An exception is thrown if there is a conflict with the namespace (two
	 * units define the same suffix).
	 */
	public void unit(UnitDefinition unit) {
		register(unit.identity(), unit);
		for (String alias : unit.aliases()) {
			register(alias, unit);
		}
	}


	private void register(String name, UnitDefinition unit) {
		UnitDefinition u = units.get(name);
		if (u != null) {
			throw abort(
			    "A suffix for a extended literal is being claimed by more than " +
			    "one type. Each suffix must be unique across all types.",
			    nv("suffix", name),
			    nv("first-type", unit.units().type()),
			    nv("second-type", u.units().type()));
		}
		units.put(name, unit);
	}


	public synchronized int register(String ident) {
		assert ! empty(ident);

		counter ++;
		registry.put(counter, ident);
		return counter;
	}


	public synchronized String source(int id) {
		if (id <= 0) {
			return "(script)";
		}
		String ident = registry.get(id);
		return ident == null ? "(unknown)" : ident;
	}


	public ConstantDefinition constant(String ident) {
		return constants.get(ident);
	}


	/**
	 * Compress this object, reducing its memory footprint as much as possible.
	 * A compressed object is still fully functional for script evaluation but
	 * may no longer possess complete metadata.
	 */
	public void compress() {
		modules.compress();
		functions.compress();
		operators.compress();
		constants.compress();
	}
}
